﻿using System;
#if !WINDOWS_PHONE
using System.Collections.Concurrent;
#endif
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
#if !NETFX_CORE
using System.Threading;
#endif
using System.Threading.Tasks;
#if NETFX_CORE
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml;
#else
using System.Windows;
#if !SILVERLIGHT
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
#endif
#endif

namespace DaveSexton.Labs
{
	[ContractVerification(false)]
#if NETFX_CORE
	public abstract partial class WindowsStoreConsoleLab
#elif WINDOWS_PHONE
	public abstract partial class PhoneConsoleLab
#elif SILVERLIGHT
	public abstract partial class SilverlightConsoleLab
#else
	public abstract partial class WindowsConsoleLab
#endif
	{
		#region Public Properties
		public bool IsRunning
		{
			get
			{
				var value = GetValue(IsRunningProperty);

				return value is bool && (bool)value;
			}
			set
			{
				SetValue(IsRunningProperty, value);
			}
		}

		public bool WaitingForUserInput
		{
			get
			{
				var value = GetValue(WaitingForUserInputProperty);

				return value is bool && (bool)value;
			}
			set
			{
				SetValue(WaitingForUserInputProperty, value);
			}
		}

		public bool HasExperiments
		{
			get
			{
				var value = GetValue(HasExperimentsProperty);

				return value is bool && (bool)value;
			}
			set
			{
				SetValue(HasExperimentsProperty, value);
			}
		}

		public IExperiment SelectedExperiment
		{
			get
			{
				return (IExperiment)GetValue(SelectedExperimentProperty);
			}
			set
			{
				SetValue(SelectedExperimentProperty, value);
			}
		}

		/// <summary>
		/// Gets or sets whether the lab should pause for user input after each experiment completes.
		/// The default value is <see langword="true" />.
		/// </summary>
		public bool PauseBetweenExperiments
		{
			get
			{
				var value = GetValue(PauseBetweenExperimentsProperty);

				return value is bool && (bool)value;
			}
			set
			{
				SetValue(PauseBetweenExperimentsProperty, value);
			}
		}
		#endregion

		#region Private / Protected
#if !SILVERLIGHT
		private ButtonBase StartButton
		{
			get
			{
				Contract.Ensures(Contract.Result<ButtonBase>() != null);

				return startButton ?? new Button();
			}
		}

		private TextBox InputTextBox
		{
			get
			{
				Contract.Ensures(Contract.Result<TextBox>() != null);

				return inputTextBox ?? new TextBox();
			}
		}
#endif

#if NETFX_CORE
		// A binding in WinRT apparently requires a public property even if it's registered as a dependency property.
		public IList<IExperiment> BindingExperiments
#else
		private IList<IExperiment> BindingExperiments
#endif
		{
			get
			{
				Contract.Ensures(Contract.Result<IList<IExperiment>>() == null || Contract.Result<IList<IExperiment>>().IsReadOnly);

#if NETFX_CORE
				var list = (IList<IExperiment>) GetValue(BindingExperimentsProperty);
#elif SILVERLIGHT
				var list = (IList<IExperiment>)GetValue(ExperimentsProperty);
#else
				var list = (IList<IExperiment>)GetValue(ExperimentsProperty.DependencyProperty);
#endif

				Contract.Assume(list == null || list.IsReadOnly);

				return list;
			}
#if NETFX_CORE
			private set
#else
			set
#endif
			{
#if NETFX_CORE
				SetValue(BindingExperimentsProperty, value);
#else
				SetValue(ExperimentsProperty, value);
#endif
			}
		}

		protected virtual IEnumerable<IExperiment> Experiments
		{
			get
			{
				Contract.Requires(HasProxy);
				Contract.Ensures(Contract.Result<IEnumerable<IExperiment>>() != null);
				Contract.Ensures(Contract.Result<IEnumerable<IExperiment>>() == Proxy.Experiments);

				return Proxy.Experiments;
			}
		}

		private readonly AsyncTaskQueue registeredTasks = new AsyncTaskQueue();
		private AnonymousExperiment mainExperiment;
		private bool pauseBetweenExperiments;
		#endregion

		#region Methods
		[ContractAbbreviator]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Contract abbreviator.")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Contract abbreviator.")]
		private void EnsuresProxyUnmodified()
		{
			Contract.Ensures(HasProxy == Contract.OldValue(HasProxy));
			Contract.Ensures(Proxy == Contract.OldValue(Proxy));
		}

		protected override void ProxyAssigned()
		{
			Contract.Ensures(HasExperiments == (BindingExperiments != null && BindingExperiments.Count > 0));
			EnsuresProxyUnmodified();

			var experiements = Proxy.OverrideExperiments(Experiments);

			if (experiements.Count > 0)
			{
				mainExperiment = new AnonymousExperiment(
					MethodExperiment.MainMethodName,
					Description ?? string.Empty,
					async () => await ExecuteLabAsync());

				BindingExperiments = new[] { mainExperiment }.Concat(experiements).ToList().AsReadOnly();
				SelectedExperiment = mainExperiment;
				HasExperiments = true;
			}
			else
			{
				BindingExperiments = null;
				SelectedExperiment = null;
				HasExperiments = false;
			}
		}

		/// <remarks>
		/// This method may seem unnecessary, but it's required for parity with the RxxLab class in the Rxx project, which uses 
		/// the ConsoleLab class and the SilverlightConsoleLab class depending upon the target framework.  In the ConsoleLab 
		/// class, this method replaces the last user input line without adding new lines to the console.
		/// </remarks>
		protected void TraceReplaceableLine(string format, params object[] args)
		{
			EnsuresProxyUnmodified();

			TraceReplaceableLine(LabTraceSource.Format(format, args));
		}

		/// <remarks>
		/// This method may seem unnecessary, but it's required for parity with the RxxLab class in the Rxx project, which uses 
		/// the ConsoleLab class and the SilverlightConsoleLab class depending upon the target framework.  In the ConsoleLab 
		/// class, this method replaces the last user input line without adding new lines to the console.
		/// </remarks>
		protected void TraceReplaceableLine(string message)
		{
			EnsuresProxyUnmodified();

			TraceLine(message);
		}

		protected void RegisterMainTask(Task task)
		{
			Contract.Requires(task != null);

			registeredTasks.Enqueue(task);
		}

		protected internal Task WhenMainCompletesAsync()
		{
			Contract.Ensures(Contract.Result<Task>() != null);

			return registeredTasks.AwaitAllAsync();
		}

		protected abstract void Main();

		protected async void RunExperiments()
		{
			await RunExperimentsAsync().ConfigureAwait(false);
		}

		protected Task RunExperimentsAsync()
		{
			Contract.Requires(HasProxy);
			EnsuresProxyUnmodified();

			return Proxy.RunExperimentsAsync(this);
		}

		private bool IsMainExperiment(IExperiment experiment)
		{
			Contract.Requires(experiment != null);

			return experiment == mainExperiment;
		}

#if NETFX_CORE
		public async void Start()
#else
		public void Start()
#endif
		{
			var host = Host;

			if (host != null)
			{
				host.CanChangeLab = false;

#if WINDOWS_PHONE
				host.Clear();
#endif
			}

			IsRunning = true;

			Contract.Assume(HasProxy);

			// This property is UI thread affine, so its value must be cached before execution begins on a pooled thread.
			pauseBetweenExperiments = PauseBetweenExperiments;

			var experiment = SelectedExperiment;

			Contract.Assume(HasExperiments || (experiment == null && mainExperiment == null));

			if (experiment == mainExperiment)
			{
#if NETFX_CORE
				await ExecuteLabAsync();
#else
				ThreadPool.QueueUserWorkItem(async _ => await ExecuteLabAsync());
#endif
			}
			else
			{
#if NETFX_CORE
				await ExecuteExperimentAsync(experiment);
#else
				ThreadPool.QueueUserWorkItem(async _ => await ExecuteExperimentAsync(experiment));
#endif
			}
		}

		private async Task ExecuteExperimentAsync(IExperiment experiment)
		{
			Contract.Requires(experiment != null);

			await Proxy.ExecuteExperimentAsync(this, experiment).ConfigureAwait(false);

#if NETFX_CORE
			await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, Stop);
#elif !SILVERLIGHT
			await Dispatcher.InvokeAsync((Action)Stop);
#else
			Dispatcher.BeginInvoke((Action)Stop);
#endif
		}

		public async Task ExecuteLabAsync()
		{
			Contract.Requires(HasProxy);
			EnsuresProxyUnmodified();

			Contract.Assert(HasProxy);

			var proxy = Proxy;

			TraceLine();
			TraceStatus(Properties.Resources.LabStartingFormat, proxy.DisplayName);
			TraceLine();

			Proxy.ResetTime();

			Main();

			await Proxy.WhenMainCompletesAsync();
			await WhenMainCompletesAsync();

			TraceLine();
			TraceStatus(Properties.Resources.LabCompletedFormat, proxy.DisplayName);

#if NETFX_CORE
			Stop();
#elif !WINDOWS_PHONE
			await Dispatcher.InvokeAsync((Action)Stop);
#else
			await Dispatcher.InvokeAsync(Stop);
#endif
		}

		private void Stop()
		{
			EnsuresProxyUnmodified();

#if SILVERLIGHT
			userSignal.Set();
#endif

			IsRunning = false;

			if (Host != null)
			{
				Host.CanChangeLab = true;
			}
		}

		partial void ExperimentSelected(IExperiment experiment);

		protected virtual async Task<bool> ExperimentStartingAsync(IList<IExperiment> experiments, int index)
		{
			Contract.Requires(HasProxy);
			Contract.Requires(experiments != null);
			Contract.Requires(experiments.IsReadOnly);
			Contract.Requires(index >= 0);
			Contract.Requires(index < experiments.Count);
			Contract.Ensures(experiments.Count == Contract.OldValue(experiments.Count));

			if (Proxy.IsRunningAllExperiments && pauseBetweenExperiments && index > 0)
			{
				TraceLine();
#if WINDOWS_PHONE
				TraceLine(Properties.Resources.PressAnyKeyToStartNextExperimentQToCancel);
#else
				TraceLine(Properties.Resources.PressAnyKeyToStartNextExperimentEscapeToCancel);
#endif
				TraceLine();

				var key = WaitForKey();

#if WINDOWS_PHONE
				if (key.Key == ConsoleKey.Q)
#else
				if (key.Key == ConsoleKey.Escape)
#endif
				{
					return false;
				}
			}
			else if (!Proxy.IsRunningAllExperiments)
			{
				TraceLine();
			}

			return await Proxy.ExperimentStartingAsync(experiments, index).ConfigureAwait(false);
		}

		protected virtual Task ExperimentExecutingAsync(IList<IExperiment> experiments, int index)
		{
			Contract.Requires(HasProxy);
			Contract.Requires(experiments != null);
			Contract.Requires(experiments.IsReadOnly);
			Contract.Requires(index >= 0);
			Contract.Requires(index < experiments.Count);
			Contract.Ensures(experiments.Count == Contract.OldValue(experiments.Count));

			return Proxy.ExperimentExecutingAsync(experiments, index);
		}

		protected virtual Task ExperimentCompletedAsync(IList<IExperiment> experiments, int index)
		{
			Contract.Requires(HasProxy);
			Contract.Requires(experiments != null);
			Contract.Requires(experiments.IsReadOnly);
			Contract.Requires(index >= 0);
			Contract.Requires(index < experiments.Count);
			Contract.Ensures(experiments.Count == Contract.OldValue(experiments.Count));

			return Proxy.ExperimentCompletedAsync(experiments, index);
		}
		#endregion

		#region Event Handlers
		private void Lab_Unloaded(object sender, RoutedEventArgs e)
		{
			Stop();
		}

		internal void StartButton_Click(object sender, RoutedEventArgs e)
		{
			Start();
		}

		private static void SelectedExperiment_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
		{
			Contract.Requires(e.NewValue != null);

			var selectedExperiment = (IExperiment)e.NewValue;

#if NETFX_CORE
			var lab = (WindowsStoreConsoleLab) sender;
#elif WINDOWS_PHONE
			var lab = (PhoneConsoleLab)sender;
#elif SILVERLIGHT
			var lab = (SilverlightConsoleLab)sender;
#else
			var lab = (WindowsConsoleLab)sender;
#endif

			Contract.Assume(lab != null);
			Contract.Assume(lab.HasProxy);

			var experiments = lab.BindingExperiments;

			if (experiments == null || !experiments.Contains(selectedExperiment))
			{
				throw new InvalidOperationException();
			}

			if (lab.IsMainExperiment(selectedExperiment))
			{
				lab.Description = lab.Proxy.Description;
			}
			else
			{
				lab.Description = selectedExperiment.Description;
			}

			lab.ExperimentSelected(selectedExperiment);
		}
		#endregion

		#region IGUIConsoleLab Members
		Task<bool> IGUIConsoleLab.ExperimentStartingAsync(IList<IExperiment> experiments, int index)
		{
			return ExperimentStartingAsync(experiments, index);
		}

		Task IGUIConsoleLab.ExperimentExecutingAsync(IList<IExperiment> experiments, int index)
		{
			return ExperimentExecutingAsync(experiments, index);
		}

		Task IGUIConsoleLab.ExperimentCompletedAsync(IList<IExperiment> experiments, int index)
		{
			return ExperimentCompletedAsync(experiments, index);
		}
		#endregion
	}
}